package com.liuqianfei.markme.rules.block;

import com.liuqianfei.markme.common.Utils;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.markme.rules.AbstractState;
import com.liuqianfei.markme.rules.IAction;
import com.liuqianfei.markme.rules.StateBlock;

import java.util.ArrayList;
import java.util.List;

/**
 * Block quotes
 * @author liuqianfei
 */
public class BlockquoteAction implements IAction
{
    @Override
    public boolean parse(AbstractState state, boolean silent, int startLine, int endLine)
    {
        boolean adjustTab = false, spaceAfterMarker, lastLineEmpty;
        List<Integer> oldBMarks = new ArrayList<>();
        List<Integer> oldBSCount = new ArrayList<>();
        List<Integer> oldSCount = new ArrayList<>();
        List<Integer> oldTShift = new ArrayList<>();
        char ch;
        int i;
        int initial;
        int l;
        int[] lines = new int[2];
        int nextLine;
        int offset;
        int oldIndent;
        String oldParentType;
        boolean terminate;
        List<IAction> terminatorRules;
        Token token;
        int pos = state.getbMarks(startLine) + state.gettShift(startLine);
        int max = state.geteMarks(startLine);

        // check the block quote marker
        if (state.getSrc().charAt(pos++) != 0x3E/* > */)
            return false;

        // we know that it's going to be a valid blockquote,
        // so no point trying to find the end of it in silent mode
        if (silent)
            return true;

        oldIndent = state.getBlkIndent();
        state.setBlkIndent(0);

        // skip spaces after ">" and re-calculate offset
        initial = offset = state.getsCount(startLine) + pos - (state.getbMarks(startLine) + state.gettShift(startLine));

        // skip one optional space after '>'
        if (state.getSrc().charAt(pos) == 0x20 /* space */)
        {
            // ' >   test '
            //     ^ -- position start of line here:
            pos++;
            initial++;
            offset++;
            adjustTab = false;
            spaceAfterMarker = true;
        }
        else if (state.getSrc().charAt(pos) == 0x09 /* tab */)
        {
            spaceAfterMarker = true;

            if ((state.getBsCount(startLine) + offset) % 4 == 3)
            {
                // '  >\t  test '
                //       ^ -- position start of line here (tab has width===1)
                pos++;
                initial++;
                offset++;
                adjustTab = false;
            }
            else
            {
                // ' >\t  test '
                //    ^ -- position start of line here + shift bsCount slightly
                //         to make extra space appear
                adjustTab = true;
            }
        }
        else
        {
            spaceAfterMarker = false;
        }

        oldBMarks.add(state.getbMarks(startLine));
        state.setbMarks(startLine, pos);

        while (pos < max)
        {
            ch = state.getSrc().charAt(pos);

            if (Utils.isSpace(ch))
            {
                if (ch == 0x09)
                {
                    offset += 4 - (offset + state.getBsCount(startLine) + (adjustTab ? 1 : 0)) % 4;
                }
                else
                {
                    offset++;
                }
            }
            else
            {
                break;
            }

            pos++;
        }

        oldBSCount.add(state.getBsCount(startLine));
        state.setBsCount(startLine, state.getsCount(startLine) + 1 + (spaceAfterMarker ? 1 : 0));

        lastLineEmpty = pos >= max;

        oldSCount.add(state.getsCount(startLine));
        state.setsCount(startLine, offset - initial);

        oldTShift.add(state.gettShift(startLine));
        state.settShift(startLine, pos - state.getbMarks(startLine));

        terminatorRules = state.getMd().parserBlock.ruler.getRules("blockquote");

        oldParentType = state.getParentType();
        state.setParentType("blockquote");

        // Search the end of the block
        //
        // Block ends with either:
        //  1. an empty line outside:
        //     ```
        //     > test
        //
        //     ```
        //  2. an empty line inside:
        //     ```
        //     >
        //     test
        //     ```
        //  3. another tag
        //     ```
        //     > test
        //      - - -
        //     ```
        for (nextLine = startLine + 1; nextLine < endLine; nextLine++)
        {
            if (state.getsCount(nextLine) < oldIndent)
                break;

            pos = state.getbMarks(nextLine) + state.gettShift(nextLine);
            max = state.geteMarks(nextLine);

            if (pos >= max)
            {
                // Case 1: line is not inside the blockquote, and this line is empty.
                break;
            }

            if (state.getSrc().charAt(pos++) == 0x3E/* > */)
            {
                // This line is inside the blockquote.

                // skip spaces after ">" and re-calculate offset
                initial = offset = state.getsCount(nextLine) + pos - (state.getbMarks(nextLine) + state.gettShift(nextLine));

                // skip one optional space after '>'
                if (state.getSrc().charAt(pos) == 0x20 /* space */)
                {
                    // ' >   test '
                    //     ^ -- position start of line here:
                    pos++;
                    initial++;
                    offset++;
                    adjustTab = false;
                    spaceAfterMarker = true;
                }
                else if (state.getSrc().charAt(pos) == 0x09 /* tab */)
                {
                    spaceAfterMarker = true;

                    if ((state.getBsCount(nextLine) + offset) % 4 == 3)
                    {
                        // '  >\t  test '
                        //       ^ -- position start of line here (tab has width===1)
                        pos++;
                        initial++;
                        offset++;
                        adjustTab = false;
                    }
                    else
                    {
                        // ' >\t  test '
                        //    ^ -- position start of line here + shift bsCount slightly
                        //         to make extra space appear
                        adjustTab = true;
                    }
                }
                else
                {
                    spaceAfterMarker = false;
                }

                oldBMarks.add(state.getbMarks(nextLine));
                state.setbMarks(nextLine, pos);

                while (pos < max)
                {
                    ch = state.getSrc().charAt(pos);

                    if (Utils.isSpace(ch))
                    {
                        if (ch == 0x09)
                            offset += 4 - (offset + state.getBsCount(nextLine) + (adjustTab ? 1 : 0)) % 4;
                        else
                            offset++;
                    }
                    else
                    {
                        break;
                    }

                    pos++;
                }

                lastLineEmpty = pos >= max;

                oldBSCount.add(state.getBsCount(nextLine));
                state.setBsCount(nextLine, state.getsCount(nextLine) + 1 + (spaceAfterMarker ? 1 : 0));

                oldSCount.add(state.getsCount(nextLine));
                state.setsCount(nextLine, offset - initial);

                oldTShift.add(state.gettShift(nextLine));
                state.settShift(nextLine, pos - state.getbMarks(nextLine));
                continue;
            }

            // Case 2: line is not inside the blockquote, and the last line was empty.
            if (lastLineEmpty)
                break;

            // Case 3: another tag found.
            terminate = false;
            for (i = 0, l = terminatorRules.size(); i < l; i++)
            {
                if (terminatorRules.get(i).parse(state, true, nextLine, endLine))
                {
                    terminate = true;
                    break;
                }
            }
            if (terminate)
                break;

            oldBMarks.add(state.getbMarks(nextLine));
            oldBSCount.add(state.getBsCount(nextLine));
            oldTShift.add(state.gettShift(nextLine));
            oldSCount.add(state.getsCount(nextLine));

            // A negative indentation means that this is a paragraph continuation
            //
            state.setsCount(nextLine, -1);
        }

        token        = state.push("blockquote_open", "blockquote", 1);
        token.markup = ">";

        lines[0] = startLine;
        lines[1] = 0;
        token.map    = lines;

        state.getMd().parserBlock.tokenize((StateBlock) state, startLine, nextLine);

        token        = state.push("blockquote_close", "blockquote", -1);
        token.markup = ">";

        state.setParentType(oldParentType);
        lines[1] = state.getLine();

        // Restore original tShift; this might not be necessary since the parser
        // has already been here, but just to make sure we can do that.
        for (i = 0; i < oldTShift.size(); i++)
        {
            state.setbMarks(i + startLine, oldBMarks.get(i));
            state.settShift(i + startLine, oldTShift.get(i));
            state.setsCount(i + startLine, oldSCount.get(i));
            state.setBsCount(i + startLine, oldBSCount.get(i));
        }
        state.setBlkIndent(oldIndent);

        return true;
    }
}
